<script lang="ts">
export default {
  name: "xt-upload-slice",
};
</script>

<script setup lang="ts">
import { ref, watch } from "vue";
import { ElUpload, UploadRawFile } from "element-plus";
import axios from "axios";
import xtButton from "../../xt-button/src/main.vue";
import xtProgress from "../../xt-progress/src/main.vue";
import useRefHook from "../../../hooks/ref";
import { formatTimestamp, getUuid, sleep } from "../../../utils/utils";
import { calculateMD5 } from "../../../utils/file";
import Typings from "../../../typings";
import { $singleMsg } from "../../../utils/element";

const { getInstance, INSTANCE } = useRefHook();

type CustomFile = UploadRawFile & {
  url: string;
  fileId: string;
  createdTime: number;
};

interface Props {
  groupId: string;
  accept?: Typings.Upload.props["accept"];
  limit?: Typings.Upload.props["limit"];
  fileSize?: number;
  disabled?: Typings.Upload.props["disabled"];
  drag?: Typings.Upload.props["drag"];
  chunkSize?: number;
}

const props = withDefaults(defineProps<Props>(), {
  accept: ".png,.jpeg,.jpg,.gif,.pdf,.doc,.docx,.xlsx,.xls", // 默认允许的上传文件类型
  groupId: "", // 文件组ID
  limit: 5, // 最大上传个数默认为5个
  fileSize: 1024 * 1024, // 1MB
  disabled: false, // 默认不置灰
  drag: false,
  chunkSize: 1024 * 50,
});

const emit = defineEmits([
  "update:fileList",
  "update:groupId",
  "success",
  "remove",
  "exceed",
]);

const FILE_LIST = ref<CustomFile[]>([]);
const uploadLoading = ref<number>(0);

// 是否阻止监听 groupId，排除第一次上传成功的情况
const shouldStopWatchGroupId = ref<boolean>(false);
watch(
  // groupId 从无到有时，请求接口获取对应的文件组文件(第一次上传成功的除外)
  () => props.groupId,
  async (n, o) => {
    if (n && !o && !shouldStopWatchGroupId.value) {
      // 获取最新的文件组文件
      getGroupIdFiles(n);
    }
  }
);

// 根据 groupId 获取最新的文件
const getGroupIdFiles = async (
  groupId: string,
  checkGroupId: boolean = false
) => {
  try {
    const params = {
      groupId,
    };
    uploadLoading.value++;
    const res = await axios.get("/file/getFileByGroupId", {
      params,
    });
    if (res.data.code === 0) {
      FILE_LIST.value = res.data.data;
      emit("update:fileList", FILE_LIST.value);

      if (checkGroupId && !res.data.data.length) {
        emit("update:groupId", "");
      }
    } else {
      $singleMsg({
        type: "error",
        message: res.data.msg,
      });
    }
  } finally {
    uploadLoading.value--;
  }
};

// 文件上传数量超出规定值时
const onExceed = (files, uploadFiles) => {
  $singleMsg({
    type: "warning",
    message: `上传文件数量不能超过 ${props.limit} 个`,
  });
  emit("exceed", files, uploadFiles);
};

// 上传前拦截处理
const beforeUpload = (rawFile) => {
  uploadLoading.value++;
  const { name, size } = rawFile;

  // 处理 accept 格式
  const accept = (props.accept || "")
    .toLowerCase()
    .replaceAll(".", "")
    .split(",");

  // 文件后缀名
  const etx = name.split(".").at(-1);

  // 文件格式限制
  if (props.accept && !accept.includes(etx.toLowerCase())) {
    $singleMsg({
      type: "warning",
      message: `当前上传文件格式不符合，上传格式仅支持 ${props.accept}`,
    });
    uploadLoading.value--;
    return false;
  }

  // 文件大小限制
  if (props.fileSize && size > props.fileSize) {
    $singleMsg({
      type: "warning",
      message: `当前上传文件大小超出规定值，上传体积不能大于 ${Math.round(
        props.fileSize / 1024 / 1024
      )}MB`,
    });
    uploadLoading.value--;
    return false;
  }
};

// 删除文件成功回调
const onRemove = (file, fileList) => {
  emit("update:fileList", fileList);
  if (!fileList.length) {
    emit("update:groupId", "");
  }
  emit("remove", file, fileList);
};

// 下载文件
const downloadFile = (file) => {
  window.open(file.url, "_blank");
};

// 删除文件并获取文件组的最新文件列表
const deleteFile = async (file) => {
  try {
    file.loading = true;
    const params = {
      groupId: file.groupId,
      fileId: file.fileId,
    };
    const res = await axios.get("/file/del", {
      params,
    });
    if (res.data.code === 0) {
      $singleMsg({
        type: "success",
        message: "删除成功",
      });
      INSTANCE.value.handleRemove(file);
    } else {
      $singleMsg({
        type: "error",
        message: res.data.msg,
      });
    }
  } finally {
    file.loading = false;
  }
};

// 获取分片，异步返回
const getSlices: (file: UploadRawFile) => Promise<Blob[]> = (file) => {
  return new Promise((resolve) => {
    const slices: Blob[] = [];

    for (let i = 0; i < file.size; i += props.chunkSize) {
      const blob: Blob = file.slice(
        i,
        Math.min(i + props.chunkSize, file.size)
      );
      slices.push(blob);
    }

    resolve(slices);
  });
};

// 上传文件切片
async function uploadSlices(
  slices: Blob[], // 切片数组
  file: UploadRawFile, // 当前上传的文件
  hashCacheInfo: Typings.Common.plainObj, // 浏览器关于当前 hash 文件的缓存信息
  hash: string, // 当前文件唯一的 hash 值
  options: Typings.Common.plainObj // onHttpRequest 自定义上传的回调信息
) {
  // 服务器应上传的切片顺序
  let serviceIndex = 0;
  // 前端上传的类型 start | uploading | end
  let uploadType = "start";

  // 遍历上传文件切片;
  for (let i = 0, len = slices.length; i < len; i++) {
    if (i > 0 && i < len - 1) {
      uploadType = "uploading";
    } else if (i === len - 1) {
      uploadType = "end";
    }

    // 当前切片顺序小于服务器的应上传切片顺序时,跳过当前遍历
    if (serviceIndex > i) continue;

    // 设置上传接口的参数
    const formData = new FormData();
    formData.append("file", slices[i]); // 切片
    formData.append("groupId", hashCacheInfo.groupId); // 文件组ID
    formData.append("fileId", hashCacheInfo.fileId); // 文件ID
    formData.append("index", i + ""); // 上传切片的顺序
    formData.append("name", file.name); // 文件名称
    formData.append("size", file.size + ""); // 切片大小
    formData.append("chunkSize", props.chunkSize + ""); // 切片大小
    formData.append("uploadType", uploadType); // 上传操作类型， start, uploading, end

    // 调用上传接口
    const res = await axios.post(`/file/slice`, formData, {
      headers: {
        "Content-Type": "multipart/form-data", // 指定表单数据类型
      },
    });

    // 设置服务器应上传的切片顺序
    if (res.data.code === 0 && res.data.data.position) {
      serviceIndex = Math.ceil(
        Number(res.data.data.position) / props.chunkSize
      );
    } else {
      serviceIndex++;
    }

    // 设置当前文件的上传进度
    const percent = (
      Math.min((serviceIndex * props.chunkSize) / file.size, 1) * 100
    ).toFixed(2);

    // el-upload 组件的进度回调狗子
    options.onProgress({ percent });

    // 最后一个切片上传成功时
    if (uploadType === "end") {
      // 清除浏览器关于 hash 文件对应的缓存信息
      window.localStorage.removeItem(hash);
      // 删除当前文件在 FILE_LIST 中对应的文件信息（没有url的）
      FILE_LIST.value = FILE_LIST.value.filter((i) => i.uid !== file.uid);
      // 添加当前文件信息到 FILE_LIST 中（有url的）
      FILE_LIST.value.push(...res.data.data);

      uploadLoading.value--;
      // 更新外部的 groupId
      emit("update:groupId", hashCacheInfo.groupId);
      emit("update:fileList", FILE_LIST.value);

      // 回调外部上传成功钩子
      emit(
        "success",
        res.data.data,
        file,
        FILE_LIST.value.filter((i) => i.url)
      );
    }
  }
}

// 获取浏览器缓存的关于 hash 文件对应的缓存信息,没有时,自动设置一个
const getHashCacheInfo = (hash, groupId, file) => {
  const cacheObj = JSON.parse(window.localStorage.getItem(hash) as string);
  let obj: any = null;
  if (!cacheObj || cacheObj.groupId !== groupId) {
    obj = {
      fileId: getUuid(),
      groupId,
      name: file.name,
    };
    window.localStorage.setItem(hash, JSON.stringify(obj));
  } else {
    obj = cacheObj;
  }
  return obj;
};

// el-upload 自定义上传文件函数
const onHttpRequest: Typings.Upload.props["httpRequest"] = async (options) => {
  // 阻止继续监听 groupId 的变化
  shouldStopWatchGroupId.value = true;
  // 优先使用 props.groupId，没有的时候新建一个
  const groupId = props.groupId || getUuid();
  // 获取当前上传的文件,每次变化都是一个
  const { file } = options;
  // 获取文件唯一的hash值,与文件的名称无关
  const hash = await calculateMD5(file);
  // 获取浏览器缓存关于 hash 文件对应的信息,没有缓存时,则设置一个待缓存的信息
  const hashCacheInfo = getHashCacheInfo(hash, groupId, file);
  FILE_LIST.value = FILE_LIST.value.filter(
    (i) => i.fileId !== hashCacheInfo.fileId
  );
  emit("update:fileList", FILE_LIST.value);
  // 获取文件切片
  const slices = await getSlices(file);
  // 上传文件切片
  await uploadSlices(slices, file, hashCacheInfo, hash, options);
};

defineExpose({
  getInstance,
});
</script>

<template>
  <el-upload
    class="xt-upload"
    ref="INSTANCE"
    action="#"
    v-model:fileList="FILE_LIST"
    :accept="accept"
    :limit="limit"
    :disabled="disabled || !!uploadLoading"
    :drag="drag"
    :data="{ groupId: groupId || getUuid() }"
    :on-exceed="onExceed"
    :on-remove="onRemove"
    :before-upload="beforeUpload"
    :http-request="onHttpRequest"
  >
    <slot>
      <xt-button
        v-if="typeof drag === 'boolean' && !drag"
        :disabled="disabled"
        :loading="!!uploadLoading"
        >上传文件</xt-button
      >
      <p v-else class="drag-tip">将文件拖拽到此处</p>
    </slot>

    <template #file="{ file }">
      <div class="file-list-container">
        <!-- 文件上传中，展示进度条 -->
        <div class="uploading-container" v-if="file.status === 'uploading'">
          <p>{{ file.name }}</p>
          <xt-progress :percentage="file.percentage" :stroke-width="2" />
        </div>

        <!-- 文件上传成功后，展示上传时间，删除按钮等 -->
        <div class="uploaded-container" v-if="file.url">
          <p class="left">
            <span @click="downloadFile(file)" class="name">{{
              file.name
            }}</span>
          </p>
          <p class="right">
            <span>{{ formatTimestamp((file as any).createdTime) }}</span>
            <xt-button
              type="text"
              :loading="(file as any).loading"
              @click="deleteFile(file)"
              >删除</xt-button
            >
          </p>
        </div>
      </div>
    </template>
  </el-upload>
</template>

<style lang="scss">
@use "../../../scss/xt-upload-slice.scss";
</style>
